<?php

namespace Louis;
use Louis\Router\RouteCollector as Router;
use Louis\Http\Request;
use Louis\Http\Response;
use Louis\Http\Headers;
use Louis\Router\RouteParser;
use Louis\Router\DataGenerator;
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2017/11/2
 * Time: 11:33
 */
class App {
    /**
     * Container
     *
     * @var Container
     */
    private $container;
    /**
     * @var \Louis\CallableResolver
     */
    protected $callableResolver;
    /**
     * @var \Louis\RouterInterface
     */
    protected $router;
    /**
     * @var callable
     */
    protected $notFoundHandler;
    /**
     * @var callable
     */
    protected $notAllowedHandler;
    /**
     * @var callable
     */
    protected $errorHandler;
    /**
     * @var callable
     */
    protected $phpErrorHandler;
    /**
     * @var array
     */
    protected $settings = [
        'httpVersion' => '1.1',
        'responseChunkSize' => 4096,
        'displayErrorDetails' => false,
    ];

    /**
     * Create new application
     *
     * @param array $settings
     */
    public function __construct(array $settings = [])
    {
        $this->addSettings($settings);
        //$this->container = new Container(["settings"=>$this->settings]);
        $this->container = new Container();
        $this->registerDefaultService();
    }

    public function registerDefaultService(){
        $this->container->add("request",function (){
            return Request::createFromGlobals($_SERVER);
        }, true);
        $this->container->add("response",function (){
            $headers = ['Content-Type' => 'text/html; charset=UTF-8'];
            return new Response('', Response::HTTP_OK, $headers);
        }, true);
        $this->container->add("router",function (){
            $container = $this->getContainer();
            $router = new Router($container);
            if (method_exists($router, 'setCallableResolver')) {
                $router->setCallableResolver($container['callableResolver']);
            }
            return $router;
        }, true);

//        $this->container->offsetSet('foundHandler', function () {
//            return new RequestResponse;
//        });
//        $this->container->offsetSet('phpErrorHandler', function ($container) {
//            return new PhpError($container->get('settings')['displayErrorDetails']);
//        });
//        $this->container->offsetSet('errorHandler', function ($container) {
//            return new Error($container->get('settings')['displayErrorDetails']);
//        });
//        $this->container->offsetSet('notFoundHandler', function () {
//            return new NotFound;
//        });
//        $this->container->offsetSet('notAllowedHandler', function () {
//            return new NotAllowed;
//        });
//        $this->container->offsetSet('callableResolver', function ($container) {
//            return new CallableResolver($container);
//        });
    }


    public function run(){
        $routeCollector = $this->container->get("router");
        $dispatcher = $routeCollector->getDispatcher();
        //var_dump($routeCollector->getData());
        //$request = $this->container->get("request");
        $response = $dispatcher->respond();
        $response->send();
    }
    /**
     * Get container
     *
     * @return Container|null
     */
    public function getContainer()
    {
        return $this->container;
    }
    /**
     * Set container
     *
     * @param Container $container
     */
    public function setContainer(Container $container)
    {
        $this->container = $container;
    }

    /********************************************************************************
     * Settings management
     *******************************************************************************/
    /**
     * Does app have a setting with given key?
     *
     * @param string $key
     * @return bool
     */
    public function hasSetting($key)
    {
        return isset($this->settings[$key]);
    }
    /**
     * Get app settings
     *
     * @return array
     */
    public function getSettings()
    {
        return $this->settings;
    }
    /**
     * Get app setting with given key
     *
     * @param string $key
     * @param mixed $defaultValue
     * @return mixed
     */
    public function getSetting($key, $defaultValue = null)
    {
        return $this->hasSetting($key) ? $this->settings[$key] : $defaultValue;
    }
    /**
     * Merge a key-value array with existing app settings
     *
     * @param array $settings
     */
    public function addSettings(array $settings)
    {
        $this->settings = array_merge($this->settings, $settings);
    }
    /**
     * Add single app setting
     *
     * @param string $key
     * @param mixed $value
     */
    public function addSetting($key, $value)
    {
        $this->settings[$key] = $value;
    }

    /********************************************************************************
     * Setter and getter methods
     *******************************************************************************/
    /**
     * Set callable resolver
     *
     * @param CallableResolver $resolver
     */
    public function setCallableResolver(CallableResolver $resolver)
    {
        $this->callableResolver = $resolver;
    }
    /**
     * Get callable resolver
     *
     * @return CallableResolver|null
     */
    public function getCallableResolver()
    {
        if (! $this->callableResolver instanceof CallableResolver) {
            $this->callableResolver = new CallableResolver($this->container);
        }
        return $this->callableResolver;
    }

    /**
     * Set router
     *
     * @param Router $router
     */
    public function setRouter(Router $router)
    {
        $this->router = $router;
    }
    /**
     * Get router
     *
     * @return Router
     */
    public function getRouter()
    {
        if (! $this->router instanceof Router) {
            $router = new Router();
            $resolver = $this->getCallableResolver();
            if ($resolver instanceof CallableResolver) {
                $router->setCallableResolver($resolver);
            }
            //$routerCacheFile = $this->getSetting('routerCacheFile', false);
            //$router->setCacheFile($routerCacheFile);
            $this->router = $router;
        }
        return $this->router;
    }

    /**
     * Set callable to handle scenarios where a suitable
     * route does not match the current request.
     *
     * This service MUST return a callable that accepts
     * two arguments:
     *
     * 1. Instance of \Psr\Http\Message\ServerRequestInterface
     * 2. Instance of \Psr\Http\Message\ResponseInterface
     *
     * The callable MUST return an instance of
     * \Psr\Http\Message\ResponseInterface.
     *
     * @param callable $handler
     */
    public function setNotFoundHandler(callable $handler)
    {
        $this->notFoundHandler = $handler;
    }

    /**
     * Get callable to handle scenarios where a suitable
     * route does not match the current request.
     *
     * @return callable|NotFound
     */
    public function getNotFoundHandler()
    {
        if (!$this->notFoundHandler) {
            $this->notFoundHandler = new NotFound();
        }

        return $this->notFoundHandler;
    }

    /**
     * Set callable to handle scenarios where a suitable
     * route matches the request URI but not the request method.
     *
     * This service MUST return a callable that accepts
     * three arguments:
     *
     * 1. Instance of \Psr\Http\Message\ServerRequestInterface
     * 2. Instance of \Psr\Http\Message\ResponseInterface
     * 3. Array of allowed HTTP methods
     *
     * The callable MUST return an instance of
     * \Psr\Http\Message\ResponseInterface.
     *
     * @param callable $handler
     */
    public function setNotAllowedHandler(callable $handler)
    {
        $this->notAllowedHandler = $handler;
    }

    /**
     * Get callable to handle scenarios where a suitable
     * route matches the request URI but not the request method.
     *
     * @return callable|NotAllowed
     */
    public function getNotAllowedHandler()
    {
        if (!$this->notAllowedHandler) {
            $this->notAllowedHandler = new NotAllowed();
        }

        return $this->notAllowedHandler;
    }

    /**
     * Set callable to handle scenarios where an error
     * occurs when processing the current request.
     *
     * This service MUST return a callable that accepts three arguments:
     *
     * 1. Instance of \Psr\Http\Message\ServerRequestInterface
     * 2. Instance of \Psr\Http\Message\ResponseInterface
     * 3. Instance of \Error
     *
     * The callable MUST return an instance of
     * \Psr\Http\Message\ResponseInterface.
     *
     * @param callable $handler
     */
    public function setErrorHandler(callable $handler)
    {
        $this->errorHandler = $handler;
    }

    /**
     * Get callable to handle scenarios where an error
     * occurs when processing the current request.
     *
     * @return callable|Error
     */
    public function getErrorHandler()
    {
        if (!$this->errorHandler) {
            $this->errorHandler = new Error($this->getSetting('displayErrorDetails'));
        }

        return $this->errorHandler;
    }

    /**
     * Set callable to handle scenarios where a PHP error
     * occurs when processing the current request.
     *
     * This service MUST return a callable that accepts three arguments:
     *
     * 1. Instance of ServerRequestInterface
     * 2. Instance of ResponseInterface
     * 3. Instance of \Error
     *
     * The callable MUST return an instance of
     * \Psr\Http\Message\ResponseInterface.
     *
     * @param callable $handler
     */
    public function setPhpErrorHandler(callable $handler)
    {
        $this->phpErrorHandler = $handler;
    }

    /**
     * Get callable to handle scenarios where a PHP error
     * occurs when processing the current request.
     *
     * @return callable|PhpError
     */
    public function getPhpErrorHandler()
    {
        if (!$this->phpErrorHandler) {
            $this->phpErrorHandler = new PhpError($this->getSetting('displayErrorDetails'));
        }

        return $this->phpErrorHandler;
    }

    /**
     * Call relevant handler from the Container if needed. If it doesn't exist,
     * then just re-throw.
     *
     * @param  Exception $e
     * @param  Request $request
     * @param  Response $response
     *
     * @return ResponseInterface
     * @throws Exception if a handler is needed and not found
     */
    protected function handleException(Exception $e, Request $request, Response $response)
    {
        if ($e instanceof MethodNotAllowedException) {
            $notAllowedHandler = $this->getNotAllowedHandler();
            return $notAllowedHandler($e->getRequest(), $e->getResponse(), $e->getAllowedMethods());
        }

        if ($e instanceof NotFoundException) {
            $notFoundHandler = $this->getNotFoundHandler();
            return $notFoundHandler($e->getRequest(), $e->getResponse());
        }

        // Other exception, use $request and $response params
        $errorHandler = $this->getErrorHandler();
        return $errorHandler($request, $response, $e);
    }

    /**
     * Call relevant handler from the Container if needed. If it doesn't exist,
     * then just re-throw.
     *
     * @param  Throwable $e
     * @param  ServerRequestInterface $request
     * @param  ResponseInterface $response
     * @return ResponseInterface
     * @throws Throwable
     */
    protected function handlePhpError(Throwable $e, ServerRequestInterface $request, ResponseInterface $response)
    {
        $handler = $this->getPhpErrorHandler();
        return $handler($request, $response, $e);
    }
}